package ui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;

import net.CaptchaFetcher;
import net.ImageCaptcha;

/**
 * The CaptchaScroller is responsible for receiving requests for a number of captchas,
 * fetching and displaying them, accepting user input for each one, and passing that input
 * back to the listener.
 */
public class CaptchaScroller extends JPanel {
	private static final int borderWidth = 4; // size of color border around each captcha
	private static final Dimension captchaSize = new Dimension(300, 57);
	private AnswerListener answerListener;
	private List<JLabel> challengesNeeded = new LinkedList<JLabel>();
	private JLabel remaining = new JLabel("0"); // yet-to-be-answered count
	public void setAnswerListener(AnswerListener answerListener) {
		this.answerListener = answerListener;
	}
	private CaptchaScroller() {
		new Thread("captcha fetcher") {
			@Override
			public void run() {
				CaptchaFetcher cf = null;
				try {
					for (;;) {
						URL changeURL;
						synchronized(CaptchaScroller.this) {
							while (newBoardURL == null && (toFetch == 0 || cf == null)) // nothing to do if we don't have a valid board
								CaptchaScroller.this.wait();
							changeURL = newBoardURL;
							newBoardURL = null;
						}
						if (changeURL != null) {
							cf = null;
							try {
								cf = new CaptchaFetcher(changeURL);
							} catch (IOException io) {
								reportError("Unable to load board: " + io.getMessage());
							} catch (CaptchaFetcher.ChallengeNotFoundError nc) {
								reportError("No captcha challenge key found in board URL.");
							}
							continue;
						}
						try {
							final ImageCaptcha ic = cf.getImageChallenge();
							SwingUtilities.invokeLater(new Runnable() {
								public void run() {
									CaptchaScroller.this.challengeRetrieved(ic);
								}
							});
							synchronized(CaptchaScroller.this) {
								if (toFetch != 0)
									toFetch--;
							}
						} catch (Exception e) {
							if (e instanceof CaptchaFetcher.ChallengeNotFoundError)
								reportError("No captcha challenge image found.");
							else
								reportError("Unable to get a captcha challenge: " + e.getMessage());
							Thread.sleep(1000); // TODO implement a better delay if throttling ever really happens
						}
					}
				} catch (InterruptedException done) {
					return;
				}
			}
		}.start();
	}
	
	/**
	 * Reflect current status.
	 */
	protected void reportError(final String status) {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				JOptionPane.showMessageDialog(CaptchaScroller.this, status, "Captcha retrieval error", JOptionPane.ERROR_MESSAGE);
			}
		});
	}

	/**
	 * One of the requested challenges was fetched, so we can attach it to an empty spot.
	 * This is called from the Swing thread.
	 */
	protected void challengeRetrieved(ImageCaptcha ic) {
		challenges.add(ic);
		updateRemaining();
		if (!challengesNeeded.isEmpty())
			challengesNeeded.remove(0).setIcon(ic.getIcon());
	}
	
	private void updateRemaining() {
		remaining.setText(Integer.toString(challenges.size()));
	}
	
	private List<ImageCaptcha> challenges = new LinkedList<ImageCaptcha>();
	/** How many are remaining to fetch and display (in the background)? */
	private int toFetch = 0;
	
	/**
	 * Change the board URL and maybe the captcha key due to that.
	 */
	public synchronized void setBoardURL(URL boardURL) {
		newBoardURL = boardURL;
		notify();
	}
	URL newBoardURL = null;
	
	/**
	 * Set us up both to display and to request more challenges than we do currently.
	 */
	public synchronized void getMoreChallenges(int num) {
		toFetch += num;
		notify();
	}
	
	/**
	 * Get more challenges, accounting for old ones that we already fetched and still apply.
	 * This is called from the Swing thread.
	 */
	public synchronized void getChallenges(int total) {
		// Don't measure challenges already delivered because they went to an old Poster.  Maybe I'll push the challenge retrieval further into here.
		if (toFetch + challenges.size() >= total)
			return;
		toFetch = total - challenges.size();
		notify();
	}

	private void unhighlight(JLabel captcha) {
		captcha.setBorder(new EmptyBorder(borderWidth, borderWidth, borderWidth, borderWidth));
	}
	
	private void highlight(JLabel captcha) {
		captcha.setBorder(new LineBorder(Color.green, borderWidth));
	}
	
	/** Create a new captcha display pane. */
	public static CaptchaScroller create() {
		final CaptchaScroller ret = new CaptchaScroller();
		ret.setLayout(new BoxLayout(ret, BoxLayout.Y_AXIS) {
			@Override // Capture size changes and be careful not to recurse.
			public void layoutContainer(Container parent) {
				// Double-check that the height changed, as that's the only time we redo it all.
				boolean heightChanged = height != parent.getHeight();
				height = parent.getHeight();
				if (!ret.performingCaptchaLayout && heightChanged)
					ret.relayoutCaptchas();
				else
					super.layoutContainer(parent);
			}
			private int height = -1;
		});
		ret.add(new JLabel("Answers remaining:"));
		ret.remaining.setFont(ret.remaining.getFont().deriveFont(20f));
		ret.add(ret.remaining);
		final JTextField textEntry = new JTextField();
		textEntry.setToolTipText("Type the highlighted captcha here, then hit enter.");
		textEntry.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				ret.captchaAnswered(e.getActionCommand());
				textEntry.setText(null);
			}
		});
		ret.add(textEntry);
		// ret.add(Box.createHorizontalStrut(captchaSize.width + borderWidth * 2)); FIXME makes layout worse but I want a minimum width...
		return ret;
	}
	private static final int internalChildren = 3; // label for remaining, remaining, textEntry

	private int entryPosition; // the highlighted entry captcha rotates from visibleCaptchas[0..-1]
	private boolean performingCaptchaLayout = false;
	private void relayoutCaptchas() {
		performingCaptchaLayout = true;
		while (!visibleCaptchas.isEmpty())
			remove(visibleCaptchas.remove(0));
		challengesNeeded.clear();
		entryPosition = 0;
		int availableHeight = getHeight();
		for (int i = 0; i < internalChildren; i++)
			availableHeight -= getComponent(i).getPreferredSize().height;
		final int totalCaptchaHeight = captchaSize.height + borderWidth * 2;
		final int totalCaptchaWidth = captchaSize.width + borderWidth * 2;
		while (visibleCaptchas.isEmpty() || availableHeight >= totalCaptchaHeight) { // at least one, even if it's occluded
			JLabel captcha = new JLabel();
			Dimension size = new Dimension(totalCaptchaWidth, totalCaptchaHeight);
			captcha.setMinimumSize(size);
			captcha.setMaximumSize(size);
			captcha.setSize(size);
			availableHeight -= totalCaptchaHeight;
			if (visibleCaptchas.isEmpty())
				// The first one visible should of course be highlighted.
				highlight(captcha);
			else
				unhighlight(captcha);
			visibleCaptchas.add(captcha);
			add(captcha);
			setupImage(captcha);
		}
		validate();
		performingCaptchaLayout = false;
	}

	private static final Icon emptyIcon = new Icon() {
		@Override
		public void paintIcon(Component c, Graphics g, int x, int y) {
			g.setColor(c.getBackground());
			g.drawRect(x, y, getIconWidth(), getIconHeight());
		}

		@Override
		public int getIconWidth() {
			return captchaSize.width;
		}

		@Override
		public int getIconHeight() {
			return captchaSize.height;
		}
	};
	private void setupImage(JLabel captcha) {
		if (challenges.size() >= visibleCaptchas.size()) {
			captcha.setIcon(challenges.get(visibleCaptchas.size() - 1).getIcon());
		} else {
			challengesNeeded.add(captcha);
			captcha.setIcon(emptyIcon);
		}
	}
	
	private ArrayList<JLabel> visibleCaptchas = new ArrayList<JLabel>();
	protected void captchaAnswered(String answer) {
		if (challenges.isEmpty())
			return;
		// Once it's answered, the challenge is purged.
		ImageCaptcha answeredChallenge = challenges.remove(0);
		updateRemaining();
		JLabel current = visibleCaptchas.get(entryPosition);
		// We rotate the entry highlight forward and arrange for a challenge image replacement for the old one.
		entryPosition = (entryPosition + 1) % visibleCaptchas.size();
		JLabel next = visibleCaptchas.get(entryPosition);
		unhighlight(current);
		current.setIcon(null);
		setupImage(current);
		highlight(next);
		answerListener.answer(answeredChallenge.challengeKey, answer);
	}

	/** When the user inputs answers, they go here. */
	public interface AnswerListener {
		/** Responses will come from a Swing context. */
		public void answer(String challengeKey, String challengeResponse);
	}
	
	public static void main(String args[]) throws Exception {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				CaptchaScroller scroller = CaptchaScroller.create();
				scroller.setAnswerListener(new AnswerListener() {
					@Override
					public void answer(String challengeKey, String challengeResponse) {
						System.err.println("challenge(" + challengeKey + "): " + challengeResponse);
					}
				});
				JFrame display = new JFrame();
				display.setContentPane(scroller);
				display.pack();
				display.setVisible(true);
				try {
					scroller.setBoardURL(new URL("http://boards.4chan.org/a/"));
				} catch (MalformedURLException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				scroller.getMoreChallenges(10);
			}
		});
	}
}
